Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/next/pages/vouchers/[id].tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2023 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { useEffect, useMemo, useState } from "react";6import Footer from "components/landing/footer";7import Header from "components/landing/header";8import Head from "components/landing/head";9import {10Alert,11Button,12Card,13Divider,14Layout,15Modal,16Space,17Table,18} from "antd";19import withCustomize from "lib/with-customize";20import { Customize } from "lib/customize";21import { Icon } from "@cocalc/frontend/components/icon";22import A from "components/misc/A";23import Loading from "components/share/loading";24import TimeAgo from "timeago-react";25import apiPost from "lib/api/post";26import Avatar from "components/account/avatar";27import type { VoucherCode } from "@cocalc/util/db-schema/vouchers";28import { stringify as csvStringify } from "csv-stringify/sync";29import { human_readable_size } from "@cocalc/util/misc";30import CodeMirror from "components/share/codemirror";31import { trunc } from "lib/share/util";32import useDatabase from "lib/hooks/database";33import Notes from "./notes";34import Help from "components/vouchers/help";35import Copyable from "components/misc/copyable";36import { DescriptionColumn } from "components/store/cart";3738function RedeemURL({ code }) {39const [url, setUrl] = useState<string>("");40useEffect(() => {41if (typeof window !== "undefined") {42setUrl(codeToUrl(code, window.location.href));43}44}, []);4546return (47<Space>48<A href={url}>49<Icon name="external-link" />50</A>{" "}51<Copyable display={`…${code}`} value={url} />52</Space>53);54}5556const COLUMNS = [57{58title: "Redeem URL (share this)",59dataIndex: "url",60key: "redeem",61render: (_, { code }) => <RedeemURL code={code} />,62},63{64title: "Code",65dataIndex: "code",66key: "code",67},68{69title: "Created",70dataIndex: "created",71key: "created",72align: "center",73render: (_, { created }) => (74<>{created == null ? "-" : <TimeAgo datetime={created} />}</>75),76},77{78title: "When Redeemed",79dataIndex: "when_redeemed",80key: "when_redeemed",81align: "center",82render: (_, { when_redeemed }) => (83<>{when_redeemed == null ? "-" : <TimeAgo datetime={when_redeemed} />}</>84),85},86{87title: "Redeemed By",88dataIndex: "redeemed_by",89key: "redeemed_by",90align: "center",91render: (_, { redeemed_by }) => (92<>{redeemed_by ? <Avatar account_id={redeemed_by} /> : undefined}</>93),94},9596{97title: "Canceled",98dataIndex: "canceled",99key: "canceled",100align: "center",101render: (_, { canceled }) => (canceled ? "Yes" : "-"),102},103{104title: "Your Private Notes",105dataIndex: "notes",106key: "notes",107render: (_, { notes, code }) => <Notes notes={notes} code={code} />,108},109] as any;110111type DownloadType = "csv" | "json";112113export default function VoucherCodes({ customize, id }) {114const database = useDatabase({ vouchers: { id, title: null, cart: null } });115const [error, setError] = useState<string>("");116const [loading, setLoading] = useState<boolean>(true);117const [data, setData] = useState<VoucherCode[] | null>(null);118const [showModal, setShowModal] = useState<DownloadType | null>(null);119120useEffect(() => {121setLoading(true);122(async () => {123try {124const { codes } = await apiPost("/vouchers/get-voucher-codes", { id });125setData(codes);126} catch (err) {127setError(`${err}`);128} finally {129setLoading(false);130}131})();132}, []);133134const allCodes = useMemo(() => {135if (!data) return [];136return data.map((x) => x.code);137}, [data]);138139const unusedCodes = useMemo(() => {140if (!data) return [];141return data.filter((x) => !x.when_redeemed).map((x) => x.code);142}, [data]);143144const usedCodes = useMemo(() => {145if (!data) return [];146return data.filter((x) => !!x.when_redeemed).map((x) => x.code);147}, [data]);148149return (150<Customize value={customize}>151<Head title={`Voucher With id=${id}`} />152<DownloadModal153data={data}154id={id}155type={showModal}156onClose={() => setShowModal(null)}157/>158<Layout>159<Header />160<Layout.Content>161<div162style={{163width: "100%",164margin: "10vh 0",165display: "flex",166justifyContent: "center",167}}168>169<Card style={{ background: "#fafafa" }}>170<Space direction="vertical" align="center">171<A href="/vouchers">172<Icon name="gift2" style={{ fontSize: "75px" }} />173</A>174<h1>Voucher: id={id}</h1>175{database.value?.vouchers?.title && (176<h3>Title: {database.value.vouchers.title}</h3>177)}178<div179style={{180width: "min(600px, 100vw)",181margin: "auto",182padding: "15px",183}}184>185{database.value?.vouchers?.cart?.map((item, n) => (186<DescriptionColumn key={n} {...item} readOnly />187))}188</div>189<Divider />190191{error && (192<Alert193type="error"194message={error}195showIcon196style={{ width: "100%", marginBottom: "30px" }}197closable198onClose={() => setError("")}199/>200)}201{loading && <Loading />}202{!loading && data && (203<div>204<div205style={{206display: "flex",207justifyContent: "center",208marginBottom: "15px",209}}210>211<Space direction="vertical">212<Space>213<div style={{ width: "200px" }}>214Copy All Codes {`(${allCodes.length})`}215</div>216<Copyable217value={allCodes.join(", ")}218inputWidth={"200px"}219/>220</Space>221<Space>222<div style={{ width: "200px" }}>223Copy Unused Codes {`(${unusedCodes.length})`}224</div>225<Copyable226value={unusedCodes.join(", ")}227inputWidth={"200px"}228/>229</Space>230<Space>231<div style={{ width: "200px" }}>232Copy Redeemed Codes {`(${usedCodes.length})`}233</div>234<Copyable235value={usedCodes.join(", ")}236inputWidth={"200px"}237/>238</Space>239<Space>240<div style={{ width: "200px" }}>241Export all data to CSV242</div>243<Button onClick={() => setShowModal("csv")}>244<Icon name="csv" /> Export to CSV...245</Button>246</Space>247<Space>248<div style={{ width: "200px" }}>249Export all data to JSON250</div>251<Button onClick={() => setShowModal("json")}>252<Icon name="js-square" /> Export to JSON...253</Button>254</Space>255</Space>256</div>257258<Table259columns={COLUMNS}260dataSource={data}261rowKey="code"262pagination={{ defaultPageSize: 50 }}263/>264</div>265)}266{!loading && data?.length == 0 && (267<div>268You have not <A href="/redeem">redeemed any vouchers</A>{" "}269yet.270</div>271)}272<Help />273</Space>274</Card>275</div>276<Footer />277</Layout.Content>278</Layout>279</Customize>280);281}282283export async function getServerSideProps(context) {284const { id } = context.params;285return await withCustomize({ context, props: { id } });286}287288function DownloadModal({ type, data, id, onClose }) {289const [data0, setData0] = useState<VoucherCode[] | null>(data);290useEffect(() => {291if (data == null) return;292if (typeof window == "undefined") return;293setData0(294data.map((x) => {295return { ...x, url: codeToUrl(x.code, window.location.href) };296}),297);298}, [data]);299const path = `vouchers-${id}.${type}`;300const content = useMemo(() => {301if (!type || data0 == null) return "";302if (type == "csv") {303const x = [COLUMNS.map((x) => x.title)].concat(304data0.map((x) => COLUMNS.map((c) => x[c.dataIndex])),305);306return csvStringify(x);307} else if (type == "json") {308return JSON.stringify(data0, undefined, 2);309}310return "";311}, [type, data0]);312313const body = useMemo(() => {314if (!type || !data) {315return null;316}317return (318<div>319<div style={{ margin: "30px", fontSize: "13pt", textAlign: "center" }}>320<a321href={URL.createObjectURL(322new Blob([content], { type: "text/plain" }),323)}324download={path}325>326Download {path} (size: {human_readable_size(content.length)})327</a>328</div>329<CodeMirror330lineNumbers={false}331content={trunc(content, 500)}332filename={path}333/>334</div>335);336}, [type, data, id]);337338return (339<Modal340open={type != null}341onCancel={onClose}342onOk={onClose}343title={<>Export all data to {type ? type.toUpperCase() : ""}</>}344>345{body}346</Modal>347);348}349350function codeToUrl(code, href): string {351let i = href.lastIndexOf("/");352i = href.lastIndexOf("/", i - 1);353return `${href.slice(0, i)}/redeem/${code}`;354}355356357